/*
 *  Copright: 2008 Philipp Leitner, Distributed Systems Group,
 *            TU Wien, leitner@infosys.tuwien.ac.at
 *
 *  This file is part of DAIOS.
 *
 *  DAIOS is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as
 *  published by the Free Software Foundation.
 * 
 *  DAIOS is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with DAIOS.  If not, see
 *  <http://www.gnu.org/licenses/>.
 * 
 */

package at.ac.tuwien.infosys.dsg.daiosPlugins.nativeInvoker;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.logging.Logger;

import javax.wsdl.WSDLException;
import javax.xml.namespace.QName;
import javax.xml.stream.XMLStreamException;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.impl.llom.OMSourcedElementImpl;

import at.ac.tuwien.infosys.dsg.daios.framework.DaiosInterceptor;
import at.ac.tuwien.infosys.dsg.daios.exceptions.AmbigiousException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.InvocationException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.NoEndpointException;
import at.ac.tuwien.infosys.dsg.daios.exceptions.UnsupportedMessageTypeException;
import at.ac.tuwien.infosys.dsg.daios.framework.DaiosInterceptor;
import at.ac.tuwien.infosys.dsg.daios.framework.PollObject;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IConstants;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IDaiosCallback;
import at.ac.tuwien.infosys.dsg.daios.framework.interfaces.IServiceFrontendImplementor;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosInputMessage;
import at.ac.tuwien.infosys.dsg.daios.framework.structs.DaiosOutputMessage;
import at.ac.tuwien.infosys.dsg.daios.util.RESTURLEncoder;
import at.ac.tuwien.infosys.dsg.daios.util.Utility;
import at.ac.tuwien.infosys.dsg.daiosPlugins.axis2.axiom.OMUtil;
import at.ac.tuwien.infosys.dsg.daiosPlugins.nativeInvoker.backend.HTTPBackend;
import at.ac.tuwien.infosys.dsg.daiosPlugins.nativeInvoker.converter.RESTConverter;

/**
 * The service invoker used to call a RESTful Web Service using the
 * native (built-in) Web Service client libraries.
 * 
 * @author philipp
 *
 */
public class NativeRESTInvoker implements IServiceFrontendImplementor {
	
	private DaiosInterceptor interceptor = null;
	private Logger log = null;
	private ThreadFactory threadFactory = null;
	private boolean postRequest = true;
	private URL endpoint = null;
	private String example = null;
	private Hashtable<String, String> httpHeaders = null;
	private Hashtable<String, String> responseHttpHeaders = null;
	
	/**
	 * Construct a new invoker that calls the service,
	 * with an example being available at a given
	 * URL (REST by example). If no example is given a
	 * HTTP GET invocation is assumed.
	 * 
	 * @param url The location of the service definition, or
	 * null in case of a HTTP GET invocation.
	 * 
	 * @throws WSDLException
	 * @throws AmbigiousException 
	 * @throws IOException
	 */
	NativeRESTInvoker(URL url, DaiosInterceptor interceptor)
		throws WSDLException, AmbigiousException, IOException {
		
		this.interceptor = interceptor;
		
		log = Logger.getLogger(IConstants.LOG_NAME);
		
		threadFactory = Executors.defaultThreadFactory();
		
		// if we did not receive a URL we assume a GET request
		if(url == null)
			postRequest = false;
		
		httpHeaders = new Hashtable<String, String>();
		
		// load example
		if(postRequest) {
			if(interceptor != null)
				interceptor.beginRESTExampleFetching(url, this);
			example = Utility.getDocumentFromURL(url);
			if(interceptor != null)
				interceptor.finishedRESTExampleFetching(url, this, example);	
		}
	}
	
	/**
	 * Do a synchronous, blocking, request-response style
	 * call. The client will be blocked until the return
	 * message is delivered.
	 * 
	 * @param message The Daios style input message.
	 * @return The Daios style output message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	public DaiosOutputMessage requestResponse(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		log.info("Doing RESTful requestResponse call with native invoker.");
		
		if(interceptor != null)
			interceptor.beginInvocation("REST", "requestResponse",
				message, this);
		
		DaiosOutputMessage output = requestResponseInvocation(message);
		
		if(interceptor != null)
			interceptor.finishedInvocation("REST", "requestResponse",
				message, this, output);		
		
		return output;
		
	}

	/**
	 * Do a synchronous, blocking, request-response style
	 * call. The client will be blocked until the return
	 * message is delivered.
	 * 
	 * @param message The Daios style input message.
	 * @return The Daios style output message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	private DaiosOutputMessage requestResponseInvocation(
			DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		try {
		
			log.info("Input Message:\n"+message.toString());
			
			// check if we have an endpoint set (we need it for REST calls!)
			if(endpoint == null)
				throw new NoEndpointException("No endpoint set for " +
						"REST invocation.");
			
			DaiosOutputMessage returnMsg = null;
			
			
			if(postRequest) {
				// prepare our invocation
				OMElement xmlBody = preparePostInvocation(message);
				// fire the request
				HTTPBackend backend = new HTTPBackend(endpoint, log);
				if(interceptor != null)
					// TODO: serializing the OM model here slows the app !!!
					interceptor.doHTTPInvocation(endpoint.toExternalForm(),
							xmlBody.toString(), this);
				InputStream responseStream = backend.httpPostInvocation(xmlBody);
				if(interceptor != null)
					// TODO: serializing the OM model here slows the app !!!
					interceptor.receiveHTTPResult(endpoint.toExternalForm(),
							xmlBody.toString(), this, /*TODO*/null);
				// save HTTP and SOAP headers for later retrieval
				responseHttpHeaders = backend.getHTTPResponseHeaders();
				if(interceptor != null)
					interceptor.beginOutputConversion(message, null,
							null, this, /*TODO*/null);
				returnMsg =
					RESTConverter.getInstance().convert(responseStream);
				if(interceptor != null)
					interceptor.finishedOutputConversion(message, null,
							null, this, /*TODO*/null, returnMsg);
			}
			
			else {
				// prepare our invocation
				prepareGetInvocation(message);
				// fire the request
				HTTPBackend backend = new HTTPBackend(endpoint, log);
				if(interceptor != null)
					interceptor.doHTTPInvocation(endpoint.toExternalForm(),null, this);
				InputStream responseStream = backend.httpGetInvocation();
				if(interceptor != null)
					interceptor.receiveHTTPResult(endpoint.toExternalForm(),
						null, this, /*TODO*/null);
				// save HTTP and SOAP headers for later retrieval
				responseHttpHeaders = backend.getHTTPResponseHeaders();
				returnMsg =
					RESTConverter.getInstance().convert(responseStream);
			}
			
			// return 
			log.info("Return message after invocation is:\n"+returnMsg);
			return returnMsg;
			
		} catch(NoEndpointException e) {
			throwInvocationException(e);
			return null;
		} catch (UnsupportedMessageTypeException e) {
			throwInvocationException(e);
			return null;
		} catch (XMLStreamException e) {
			throwInvocationException(e);
			return null;
		}
		
	}

	/**
	 * 
	 * Do an asynchronous 'fire and forget'-style call.
	 * The client fires the input message, and does not
	 * expect any kind of answer or confirmation from the
	 * service.
	 * 
	 * @param message The Daios style input message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	public void fireAndForget(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		log.info("Doing RESTful fireAndForget call with native invoker.");
		
		if(interceptor != null)
			interceptor.beginInvocation("REST", "fireandforget",
				message, this);
		
		fireAndForgetInvocation(message);
		
		if(interceptor != null)
			interceptor.finishedInvocation("REST", "fireandforget",
				message, this, null);		
			
	}

	/**
	 * 
	 * Do an asynchronous 'fire and forget'-style call.
	 * The client fires the input message, and does not
	 * expect any kind of answer or confirmation from the
	 * service.
	 * 
	 * @param message The Daios style input message.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	private void fireAndForgetInvocation(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		try {
		
			log.info("Input Message:\n"+message.toString());
			
			// check if we have an endpoint set (we need it for REST calls!)
			if(endpoint == null)
				throw new NoEndpointException("No endpoint set for " +
						"REST invocation.");
			
			if(postRequest) {
				// prepare our invocation
				OMElement xmlBody = preparePostInvocation(message);
				// fire the request
				HTTPBackend backend = new HTTPBackend(endpoint, log);
				if(interceptor != null)
					interceptor.doHTTPInvocation(endpoint.toExternalForm(),
						/*TODO*/null, this);
				backend.httpPostInvocation(xmlBody);
			}
			else {
				// prepare our invocation				
				prepareGetInvocation(message);
				// fire the request
				HTTPBackend backend = new HTTPBackend(endpoint, log);
				if(interceptor != null)
					interceptor.doHTTPInvocation(endpoint.toExternalForm(),
						null, this);
				backend.httpGetInvocation();
			}
			
		} catch(NoEndpointException e) {
			throwInvocationException(e);
		} catch (UnsupportedMessageTypeException e) {
			throwInvocationException(e);
		} catch (XMLStreamException e) {
			throwInvocationException(e);
		}
		
	}

	/**
	 * Do an asynchronous 'callback'-style call.
	 * The client fires the input message, and registers
	 * a <i>callback listener</i> which is notified
	 * when the response is available. The listener
	 * has to implement the interface
	 * <link>at.ac.tuwien.infosys.dsg.daios.framework.interfaces.DaiosCallback</link>
	 * and implement the <i>onComplete</i> and <i>onError</i>
	 * methods accordingly.
	 * 
	 * @param message The Daios-style input message.
	 * @param callback The registered callback listener.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	public void callback(DaiosInputMessage message, IDaiosCallback callback)
		throws AmbigiousException, InvocationException {
		
		log.info("Doing native REST callback call with message:\n"
				+message.toString());
		
		if(interceptor != null)
			interceptor.beginInvocation("REST", "callback",
				message, this);
		
		callbackInvocation(message, callback);
		
	}

	/**
	 * Do an asynchronous 'callback'-style call.
	 * The client fires the input message, and registers
	 * a <i>callback listener</i> which is notified
	 * when the response is available. The listener
	 * has to implement the interface
	 * <link>at.ac.tuwien.infosys.dsg.daios.framework.interfaces.DaiosCallback</link>
	 * and implement the <i>onComplete</i> and <i>onError</i>
	 * methods accordingly.
	 * 
	 * @param message The Daios-style input message.
	 * @param callback The registered callback listener.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */	
	private void callbackInvocation(DaiosInputMessage message,
			IDaiosCallback callback) {
		
		// create new thread
		threadFactory.newThread(new AsyncCaller(message, callback))
			.start();
		
	}

	/**
	 * Do an asynchronous 'poll object'-style call. The client
	 * fires the input message, and receives a 'poll object'
	 * as response. This poll object can be 'queried' for the
	 * result of the invocation as soon as it is available.
	 * 
	 * <p>
	 * 
	 * The poll object call is an abstraction of the more
	 * general callback call.
	 * 
	 * @param message The Daios-style input message.
	 * @return The poll object which will contain the result
	 * as soon as it is available.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */		
	public PollObject pollObjectCall(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		log.info("Doing native REST Poll Object call with message:\n"
				+message.toString());
		
		if(interceptor != null)
			interceptor.beginInvocation("REST", "pollobject",
				message, this);
		
		return pollObjectInvocation(message);
		
	}

	/**
	 * Do an asynchronous 'poll object'-style call. The client
	 * fires the input message, and receives a 'poll object'
	 * as response. This poll object can be 'queried' for the
	 * result of the invocation as soon as it is available.
	 * 
	 * <p>
	 * 
	 * The poll object call is an abstraction of the more
	 * general callback call.
	 * 
	 * @param message The Daios-style input message.
	 * @return The poll object which will contain the result
	 * as soon as it is available.
	 * 
	 * @throws AmbigiousException
	 * @throws InvocationException
	 */		
	private PollObject pollObjectInvocation(DaiosInputMessage message)
		throws AmbigiousException, InvocationException {
		
		NativePollObject po = new NativePollObject();
		
		NativeCallbackHandler callback = new
			NativeCallbackHandler(po);
		
		callbackInvocation(message, callback);
		
		return po;
		
	}

	/**
	 * Set the endpoint address of the service. Note that
	 * it is mandatory to use this method for RESTful invocations.
	 * 
	 * @param endpoint The address of the endpoint.
	 */		
	public void setEndpointAddress(URL endpoint) {
		
		this.endpoint = endpoint;
		
	}

	/**
	 * Not supported by REST. 
	 */
	public void setOperation(QName operation) throws AmbigiousException {
		
		throw new UnsupportedOperationException
			("setOperation  not supported in REST invocation.");
		
	}

	/**
	 * Not supported by REST. 
	 */	
	public void setInputMessage(QName message) throws AmbigiousException {
		
		throw new UnsupportedOperationException
			("setInputMessage  not supported in REST invocation.");
		
	}
	
	/**
	 * Set HTTP method to use (HTTP GET or HTTP POST).
	 * 
	 * @param method
	 */
	public void setHTTPMethod(String method) {
		
		if(method.equalsIgnoreCase(IConstants.HTTP_GET_METHOD))
			postRequest = false;
		else {
			if(method.equalsIgnoreCase(IConstants.HTTP_POST_METHOD))
				postRequest = true;
			else
				throw new IllegalArgumentException("HTTP method "+method+" is "+
					"not supported.");
		}
	}
	
	/**
	 * Add a certain HTTP header to the invocation.
	 * 
	 * @param header The String name of the header to set.
	 * @param value The String value of the header.
	 * @return For simplicity this method returns the invoker.
	 */		
	public NativeRESTInvoker addHTTPHeader(String header, String value) {
		httpHeaders.put(header, value);
		return this;
	}
	
	/**
	 * Remove a certain HTTP header.
	 * 
	 * @param header The String name of the header to remove.
	 * @return For simplicity this method returns the invoker.
	 */			
	public NativeRESTInvoker removeHTTPHeader(String header) {
		httpHeaders.remove(header);
		return this;
	}

	/**
	 * Get a list of all current header names.
	 * 
	 * @return A list of all header names.
	 */	
	public Enumeration<String> getHTTPHeaders() {
		return httpHeaders.keys();
	}	
	
	/**
	 * Get a table containg all response HTTP headers. This
	 * method can be used only <i>after</i> the invocation
	 * has been fired. If it is used before the first invocation
	 * it will return null.
	 * 
	 * @return A table containing all HTTP response headers.
	 */	
	public Hashtable<String, String> getResponseHTTPHeaders() {
		return responseHttpHeaders;
	}		

	/**
	 * Hook an interceptor into this frontend.
	 * 
	 * @param interceptor
	 */	
	public void setInterceptor(DaiosInterceptor interceptor) {
		this.interceptor = interceptor;
	}
	
	/**
	 * Prepare a HTTP GET invocation. The most important step
	 * carried out here is the conversion of the input message
	 * to URL parameters (i.e. the endpoint is modified with
	 * request parameters '?param1=value1&param2=value2').
	 * 
	 * @param message The input message to use.
	 * @throws UnsupportedMessageTypeException
	 */
	private void prepareGetInvocation(DaiosInputMessage message)
		throws UnsupportedMessageTypeException {
		
		log.info("Using HTTP GET for REST call.");
		
		// encode the message as URL
		endpoint = RESTURLEncoder.encodeToURL(
			endpoint, message);
		
	}

	/**
	 * Prepare a HTTP POST invocation. Convert the input
	 * message given to XML (according to a given example,
	 * REST by example).
	 * 
	 * @param message The input message to use.
	 * @throws UnsupportedMessageTypeException
	 */	
	private OMElement preparePostInvocation(DaiosInputMessage message)
		throws XMLStreamException, AmbigiousException {
		
		log.info("Using HTTP POST for REST call.");
		
		if(interceptor != null)
			interceptor.beginInputConversion(message, null, null, null, this);
		
		RESTConverter converter = RESTConverter.getInstance();
		OMElement exampleRoot = null;
		OMElement xmlBody = null;
		
		// convert the message to XML using the example
		exampleRoot = OMUtil.getRootNode(example);
		xmlBody = converter.convert(message, exampleRoot);	
		
		if(interceptor != null)
			interceptor.finishedInputConversion(message, null,
					null, null, this, /*TODO*/null);		
		
		return xmlBody;
		
	}
	
	/**
	 * An error occured. Throw an InvocationException.
	 * 
	 * @param e
	 * @throws InvocationException
	 */
	private void throwInvocationException(Exception e)
		throws InvocationException {
		
		log.severe("Exception thrown during Daios processing:");
		e.printStackTrace();
		throw new InvocationException(e);
		
	}
	
	/**
	 * Private thread used to fire asynchronous requests.
	 * 
	 * @author philipp
	 *
	 */
	class AsyncCaller implements Runnable {
		
		private IDaiosCallback callback = null;
		private DaiosInputMessage input = null;
		
		/**
		 * Construct a new thread.
		 * 
		 * @param input The input message to use.
		 * @param callback The callback interface that should
		 * be notified when a response comes in.
		 */		
		public AsyncCaller(DaiosInputMessage input, IDaiosCallback callback) {
			this.callback = callback;
			this.input = input;
		}
		
		/**
		 * Run the thread.
		 */
		public void run() {
			
			try {
				// we can do a simple 'request/response'
				// invocation here in our separated thread
				DaiosOutputMessage output = 
					requestResponseInvocation(input);
				if(interceptor != null)
					interceptor.finishedInvocation("REST", "callback",
						input, NativeRESTInvoker.this, output);				
				// notify listener
				callback.onComplete(output);
			} catch (Exception e) {
				e.printStackTrace();
				callback.onError(e);
			} 
			
		}
		
	}

}
